package main

import (
	"encoding/json"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"syscall"
	"time"
)

type Resp struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
	Data string `json:"data"`
}

type Id struct {
	Id int `json:"id"`
}

type Job struct {
	Name string `json:"name"`
	Val  string `json:"val"`
	App  string `json:"app"`
}

type Cron struct {
	Id  int `json:"id"`
	Val Job `json:"val"`
}

type UpdateCron struct {
	Id   int    `json:"id"`
	Name string `json:"name"`
	Val  string `json:"val"`
	App  string `json:"app"`
}

var configFileName = "_config.json"
var appRunLogFile = "_command_error.log"
var appRunLogOkFile = "_command_success.log"
var maxId = 0
var crons []Cron

// *,17,3 7,6,8,9 7,9,7,3 * * *
func parseUnit(s string, vInt int) bool {

	if len(s) == 0 {
		return false
	}

	if s == "*" {
		return true
	}

	tryInt, e := strconv.Atoi(s)
	if e == nil && vInt == tryInt {
		return true
	}

	var startIndex = 0
	var str string
	strLen := len(s)

	for k := startIndex; k < strLen; k++ {
		v := s[k]

		if v == ',' {
			str = s[startIndex:k]
			if str == "*" {
				return true
			}
			strInt, err := strconv.Atoi(str)
			if err != nil {
				return false
			}
			if vInt == strInt {
				return true
			}
			startIndex = k + 1
		} else if v == '-' {

			strInt, err := strconv.Atoi(s[startIndex:k])
			if err != nil {
				return false
			}
			var sliceIndex = k + 1
			for j := sliceIndex; j < strLen; j++ {
				v = s[j]
				k++
				if v == ' ' || v == ',' || j == strLen-1 {
					if j == strLen-1 {
						j++
					}
					strInt2, e := strconv.Atoi(s[sliceIndex:j])
					if e != nil {
						return false
					}
					if vInt >= strInt && vInt <= strInt2 {
						return true
					}

					break
				}
			}
			startIndex = k + 1
		} else if v == '/' {

			var sliceIndex = k + 1
			for j := sliceIndex; j < strLen; j++ {
				v = s[j]
				k++
				if v == ' ' || v == ',' || j == strLen-1 {
					if j == strLen-1 {
						j++
					}
					strInt2, e := strconv.Atoi(s[sliceIndex:j])
					if e != nil {
						return false
					}
					if vInt%strInt2 == 0 {
						return true
					}
					break
				}
			}
			startIndex = k + 1
		}
	}

	return false
}

// 解析字符串，判断是否匹配当前时间
func parseCronStr(str string) bool {
	var nowTime = time.Now()
	var month, day, hour, minute, second = int(nowTime.Month()), nowTime.Day(),
		nowTime.Hour(), nowTime.Minute(),
		nowTime.Second()
	_, week := nowTime.ISOWeek()
	if len(str) == 0 {
		return false
	}

	cron := strings.Split(str, " ")
	if len(cron) == 5 {
		return parseUnit("1", second) &&
			parseUnit(cron[0], minute) &&
			parseUnit(cron[1], hour) &&
			parseUnit(cron[2], day) &&
			parseUnit(cron[3], month) &&
			parseUnit(cron[4], week)
	} else if len(cron) == 6 {
		return parseUnit(cron[0], second) &&
			parseUnit(cron[1], minute) &&
			parseUnit(cron[2], hour) &&
			parseUnit(cron[3], day) &&
			parseUnit(cron[4], month) &&
			parseUnit(cron[5], week)
	}
	return false
}

// 响应成功
func RespOK(w http.ResponseWriter, v string) {
	d, _ := json.Marshal(Resp{
		Code: 200,
		Msg:  "SUCCESS",
		Data: v,
	})
	w.Header().Add("Content-Type", "application/json;charset=utf-8")
	_, _ = w.Write(d)
}

// 响应失败
func RespErr(w http.ResponseWriter, v string) {
	d, _ := json.Marshal(Resp{
		Code: 500,
		Msg:  "ERROR",
		Data: v,
	})
	w.Header().Add("Content-Type", "application/json;charset=utf-8")
	_, _ = w.Write(d)
}

// Not used
func GetRequestStr(w http.ResponseWriter, v interface{}, r *http.Request) interface{} {
	s, _ := ioutil.ReadAll(r.Body)
	err := json.Unmarshal(s, v)
	if err != nil {
		RespErr(w, "请求包体不合法,请校验请求数据!")
		return nil
	}
	return v
}

// 判断文件是否存在
func checkFileExist(file string) bool {
	if _, err := os.Stat(file); os.IsExist(err) {
		return false
	}
	return true
}

// 获取所有的定时任务
func GetAllCrons() []Cron {
	if !checkFileExist(configFileName) {
		log.Fatal("File: " + configFileName + " not found!")
	}

	var confArr []Cron

	f, _ := os.OpenFile(configFileName, os.O_APPEND, 0666)
	byteArr, _ := ioutil.ReadAll(f)
	if len(byteArr) == 0 {
		return confArr
	}

	err := json.Unmarshal(byteArr, &confArr)
	if err != nil {
		log.Fatal(configFileName + " content format wrong!")
	}
	return confArr
}

// 更新文件内容
func UpdateDb(b []byte, a bool) bool {
	if !checkFileExist(configFileName) {
		log.Fatal(configFileName + " not found!")
	}
	var flag int
	flag = os.O_WRONLY | os.O_SYNC
	if a {
		flag |= os.O_APPEND
	} else {
		flag |= os.O_TRUNC
	}
	f, _ := os.OpenFile(configFileName, flag, 0666)
	_, err := io.WriteString(f, string(b))
	_ = syscall.Sync()
	if err != nil {
		return false
	}
	return true
}

// 默认请求
func defaultResponse(w http.ResponseWriter, r *http.Request) {
	_, _ = w.Write([]byte("Welcome to use cknit!"))
}

// 更新定时任务
func UpdateJob(w http.ResponseWriter, r *http.Request) {
	s, _ := ioutil.ReadAll(r.Body)
	var updateCron UpdateCron
	err := json.Unmarshal(s, &updateCron)
	if err != nil {
		RespErr(w, "请求包体不合法,请校验请求数据!")
		return
	}

	var res = make([]Cron, 0)
	for _, d := range crons {
		if d.Id == updateCron.Id {
			res = append(res, Cron{
				Id: d.Id,
				Val: Job{
					Name: updateCron.Name,
					Val:  updateCron.Val,
					App:  updateCron.App,
				},
			})
		} else {
			res = append(res, Cron{
				Id:  d.Id,
				Val: d.Val,
			})
		}
	}
	crons = res

	// 写入文件，进行sync同步操作
	b, _ := json.Marshal(res)
	if UpdateDb(b, false) {
		RespOK(w, string(b))
	} else {
		RespErr(w, string("系统错误!"))
	}
}

// 新增定时任务
func AddJob(w http.ResponseWriter, r *http.Request) {
	s, _ := ioutil.ReadAll(r.Body)
	var updateCron UpdateCron
	err := json.Unmarshal(s, &updateCron)
	if err != nil {
		RespErr(w, "请求包体不合法,请校验请求数据!")
		return
	}

	var cron = Cron{
		Id: maxId,
		Val: Job{
			Name: updateCron.Name,
			Val:  updateCron.Val,
			App:  updateCron.App,
		},
	}
	crons = append(crons, cron)
	res, _ := json.Marshal(crons)
	if UpdateDb(res, false) {
		maxId++
		RespOK(w, string(s))
	} else {
		RespErr(w, string("系统错误!"))
	}
}

// 删除定时任务
func DeleteJob(w http.ResponseWriter, r *http.Request) {
	s, _ := ioutil.ReadAll(r.Body)
	var id Id
	err := json.Unmarshal(s, &id)
	if err != nil {
		RespErr(w, "请求包体不合法,请校验请求数据!")
		return
	}

	var res = make([]Cron, 0)
	for _, v := range crons {
		if v.Id == id.Id {
			continue
		} else {
			res = append(res, Cron{
				Id:  v.Id,
				Val: v.Val,
			})
		}
	}
	crons = res

	// 写入文件，进行sync同步操作
	b, _ := json.Marshal(res)
	if UpdateDb(b, false) {
		RespOK(w, string(b))
	} else {
		RespErr(w, string("系统错误!"))
	}
}

// 执行增删改查操作
func Do(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case "GET": // 获取定时任务
		b, _ := json.Marshal(crons)
		RespOK(w, string(b))
	case "PUT": // 修改定时任务
		UpdateJob(w, r)
	case "POST": // 新增定时任务
		AddJob(w, r)
	case "DELETE": // 删除定时任务
		DeleteJob(w, r)
	}
}

// 增加日志
func AppendLog(job *Job, file string, info string) {
	f, _ := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_APPEND|os.O_SYNC, 0666)
	if len(info) == 0 {
		info = " [Warning: No console output]\n"
	}
	var data = time.Now().Format("2006-01-02 15:04:05") + " " + job.Name + " " + job.App + " " + info
	_, _ = io.WriteString(f, data)
}

// 执行定时任务
func doJob(jobs *[]Cron) {
	if len(*jobs) == 0 {
		return
	}
	for _, job := range *jobs {
		if parseCronStr(job.Val.Val) {
			commandArr := strings.Split(job.Val.App, " ")
			cmd := exec.Command(commandArr[0], commandArr[1])
			stdout, err := cmd.StdoutPipe()
			if err != nil {
				AppendLog(&job.Val, appRunLogFile, err.Error())
				continue
			}
			if err := cmd.Start(); err != nil {
				AppendLog(&job.Val, appRunLogFile, err.Error())
				continue
			}
			if opBytes, err := ioutil.ReadAll(stdout); err != nil {
				AppendLog(&job.Val, appRunLogFile, err.Error())
				continue
			} else {
				AppendLog(&job.Val, appRunLogOkFile, string(opBytes))
			}
			stdout.Close()
		}
	}
}

// 执行定时任务
func DoCron() {
	t := time.Tick(time.Second)
	for {
		select {
		case <-t:
			go doJob(&crons)
		}
	}
}

// 入口
func main() {

	go DoCron()

	crons = GetAllCrons()
	http.HandleFunc("/", defaultResponse)
	http.HandleFunc("/do", Do)

	for _, v := range crons {
		if maxId == 0 {
			maxId = v.Id
		}

		if v.Id > maxId {
			maxId = v.Id
		}
	}
	maxId++
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatal("Can't create socket and listen!")
	}
}
